package cn.cosmosx.base.encrypt.loader;

import cn.cosmosx.base.encrypt.hex.HexUtil;
import cn.cosmosx.base.encrypt.sm.Provider;
import cn.cosmosx.base.util.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.misc.MiscObjectIdentifiers;
import org.bouncycastle.asn1.misc.NetscapeCertType;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.X509KeyUsage;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.encoders.Hex;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.Calendar;
import java.util.Date;

/**
 * SM2 PFX（pkcs12）证书生成工具
 */
@Slf4j
public final class SM2PFXCertGenerateUtil {
    /******************证书信息START******************/
    // 国家
    private final static String COUNTRY = "CN";
    // 组织
    private final static String ORG = "COSMOSX-CN";
    // 省份
    private final static String PROVINCE = "Guizhou";
    // 地区
    private final static String LOCATION = "Guiyang";
    private final static Date effectiveDate = Calendar.getInstance().getTime();
    static Calendar expiredCalendar;

    static {
        expiredCalendar = Calendar.getInstance();
        expiredCalendar.add(Calendar.YEAR, 1);
    }

    private final static Date expiredDate = expiredCalendar.getTime();

    /******************证书信息END********************/

    private static KeyPair keyPair;

    /**
     * BouncyCastle算法提供者
     */
    private static final BouncyCastleProvider PROVIDER = Provider.INSTANCE.getProvider();
    private static final String RESOURCES_PATH = System.getProperty("user.dir") + File.separator + "src/main/resources" + File.separator;

    /**
     * 生成SM2 pfx证书文件并写入到Response流中
     * <p>
     * 里面包括公钥，私钥，证书别名
     * <p>
     * 注：证书公钥为未压缩格式
     *
     * @param alias    - 证书别名（自定义）
     * @param password - 证书访问密码（自定义）
     */
    public static void savePfx(String alias, String password, String certFileName, HttpServletResponse response) throws Exception {
        // 构建证书
        X509Certificate certificate = buildX509Certificate();
        // 私钥
        BCECPrivateKey ecPrivateKey = (BCECPrivateKey) keyPair.getPrivate();
        BigInteger privateKey = ecPrivateKey.getD();
        // 公钥
        BCECPublicKey ecPublicKey = (BCECPublicKey) keyPair.getPublic();
        ECPoint publicKey = ecPublicKey.getQ();
        log.info("SM2 证书私钥: {}", HexUtil.toHexString(privateKey.toByteArray()));
        log.info("SM2 证书公钥: {}", HexUtil.toHexString(publicKey.getEncoded(false)));
        // 保存证书文件
        KeyStore outputKeyStore = KeyStore.getInstance("pkcs12", PROVIDER);
        outputKeyStore.load(null, password.toCharArray());
        outputKeyStore.setKeyEntry(alias, ecPrivateKey, password.toCharArray(), new Certificate[]{certificate});
        // 设置响应头参数
        ResponseUtils.setBlobHeader(response, certFileName);
        outputKeyStore.store(response.getOutputStream(), password.toCharArray());
    }

    /**
     * 生成并保存SM2 pfx证书文件。默认生成到/resources/certificate/资源路径下
     * <p>
     * 里面包括公钥，私钥，证书别名
     * <p>
     * 注：证书公钥为未压缩格式
     *
     * @param alias    - 证书别名（自定义）
     * @param password - 证书访问密码（自定义）
     */
    public static void savePfx(String alias, String password) throws Exception {
        String path = RESOURCES_PATH + "certificate/cert" + System.nanoTime() + ".pfx";
        savePfx(alias, password, path);
    }

    /**
     * 生成并保存SM2 pfx证书文件
     * <p>
     * 里面包括公钥，私钥，证书别名。
     * <p>
     * 注：证书公钥为未压缩格式
     *
     * @param alias    - 证书别名（自定义）
     * @param password - 证书访问密码（自定义）
     * @param filepath - 证书保存路径（文件名建议使用.pfx，即pkcs12证书格式）
     */
    public static void savePfx(String alias, String password, String filepath) throws Exception {
        try (FileOutputStream out = new FileOutputStream(filepath)) {
            // 构建证书
            X509Certificate certificate = buildX509Certificate();
            // 私钥
            BCECPrivateKey ecPrivateKey = (BCECPrivateKey) keyPair.getPrivate();
            BigInteger privateKey = ecPrivateKey.getD();
            // 公钥
            BCECPublicKey ecPublicKey = (BCECPublicKey) keyPair.getPublic();
            ECPoint publicKey = ecPublicKey.getQ();
            log.info("SM2 证书私钥: {}", HexUtil.toHexString(privateKey.toByteArray()));
            log.info("SM2 证书公钥: {}", HexUtil.toHexString(publicKey.getEncoded(false)));
            // 保存证书文件
            KeyStore outputKeyStore = KeyStore.getInstance("pkcs12", PROVIDER);
            outputKeyStore.load(null, password.toCharArray());
            outputKeyStore.setKeyEntry(alias, ecPrivateKey, password.toCharArray(), new Certificate[]{certificate});
            outputKeyStore.store(out, password.toCharArray());
        }
    }

    /**
     * X.509格式证书对象生成，通过使用证书构造好
     * 将证书构造参数装换为X.509证书对象
     */
    private static X509Certificate buildX509Certificate() throws Exception {
        // 产生公私钥对
        keyPair = getSM2KeyPair();
        // 私钥
        BCECPrivateKey ecPrivateKey = (BCECPrivateKey) keyPair.getPrivate();
        // 公钥
        BCECPublicKey ecPublicKey = (BCECPublicKey) keyPair.getPublic();

        // 证书签名算法算法提供者
        // 在制作证书时需要使用到签名算法签名证书中部分数据区域，国密类型的数字证书使用的签名算法是SM3withSM2，这里使用私钥创建算法提供容器。
        ContentSigner sigGen = new JcaContentSignerBuilder("SM3withSM2").setProvider(PROVIDER).build(ecPrivateKey);

        // 证书信息构造，使用X509v3CertificateBuilder 来设置证书的基本参数，下面列举基本一些证书参数和扩展参数的设置方式。构造X.509 第3版的证书构建者
        X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(
                // 颁发者信息
                createStdBuilder().build()
                // 证书序列号
                , BigInteger.valueOf(1)
                // 证书生效日期
                , effectiveDate
                // 证书失效日期
                , expiredDate
                // 使用者信息（PS：由于是自签证书，所以颁发者和使用者DN都相同）
                , createStdBuilder().build()
                // 证书公钥
                , ecPublicKey)
                // 设置证书扩展。
                // 证书扩展属性，请根据需求设定，参数请参考 《RFC 5280》 设置密钥用法
                .addExtension(Extension.keyUsage, false, new X509KeyUsage(X509KeyUsage.digitalSignature | X509KeyUsage.nonRepudiation))
                // 设置扩展密钥用法：客户端身份认证、安全电子邮件、签名
                .addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage())
                // 基础约束,标识是否是CA证书，这里false标识为实体证书
                .addExtension(Extension.basicConstraints, false, new BasicConstraints(false))
                // Netscape Cert Type SSL客户端身份认证
                .addExtension(MiscObjectIdentifiers.netscapeCertType, false, new NetscapeCertType(NetscapeCertType.sslClient));

        // X.509格式证书对象生成，通过使用证书构造好的信息，接下就能够生成x509格式的证书对象
        // 将证书构造参数装换为X.509证书对象
        return new JcaX509CertificateConverter().setProvider(PROVIDER).getCertificate(certGen.build(sigGen));
    }

    /**
     * 设置证书信息
     * 设置证书的基本数据：使用者信息、颁发者信息、证书序号、证书生效日期、证书失效日期，以及证书扩展属性。
     * <p>
     * 标识信息构造（DN）
     * 上面提到的使用者信息、颁发者信息，使用Distinct Name的方式来描述
     * <p>
     * 实际上简单来讲就是，用来确定“实体” 身份/信息 的一系列列键值对组成的字符串。
     * 这里的键是一个ASN1ObjectIdentifier，实际上Bouncycastle已经为我们把需要的大多键都已经列好了，
     * 我们只要使用这个类org.bouncycastle.asn1.x500.style.BCStyle的静态变量就可以。
     */
    private static X500NameBuilder createStdBuilder() {
        X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
        // 国家代码
        builder.addRDN(BCStyle.C, COUNTRY);
        // 组织
        builder.addRDN(BCStyle.O, ORG);
        // 省份
        builder.addRDN(BCStyle.ST, PROVINCE);
        // 地区
        builder.addRDN(BCStyle.L, LOCATION);
        return builder;
    }

    /**
     * 获取扩展密钥用途构造（可选）
     * 如果需要设置证书的扩展密钥用途，可以使用DERSequence来构造一个拓展密钥用途的序列。
     */
    private static DERSequence extendedKeyUsage() {
        // 构造容器对象
        ASN1EncodableVector vector = new ASN1EncodableVector();
        // 客户端身份认证
        vector.add(KeyPurposeId.id_kp_clientAuth);
        // 安全电子邮件
        vector.add(KeyPurposeId.id_kp_emailProtection);
        // 签名
        vector.add(KeyPurposeId.id_kp_codeSigning);
        return new DERSequence(vector);
    }

    /**
     * 产生SM2秘钥对
     */
    private static KeyPair getSM2KeyPair() throws Exception {
        // 获取SM2椭圆曲线的参数
        final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
        // 获取一个椭圆曲线类型的密钥对生成器
        final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", PROVIDER);
        // 使用SM2的算法区域初始化密钥生成器
        kpg.initialize(sm2Spec, new SecureRandom());
        // 获取密钥对
        return kpg.generateKeyPair();
    }

}
